
package system.communication;

import system.track.Direction;
import system.track.Sensor;
import system.track.SwitchAngle;
import java.io.IOException;
import java.io.PrintStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;
import system.ui.Engine;

/**
 * The Monitor and uses the Singleton object-oriented pattern. All Messages to the train are handled with the Monitor. 
 * The MessageListener places all Reply messages and Sensor trip messages into the Monitor's respective lists
 * and the Monitor will get the Messages out of its list to return specificied information
 * based on Input. The Monitor must wait for the MessageListener to retreive a reply
 * from the Train before sending the next message and thus is put on wait after printing
 * to the socket. The MessageListener will notify the Monitor to wake after it receives
 * and pushes the Message to the Monitor. 
 * 
 * To use the Monitor you must use the getInstance method in order to call other methods within
 * the CMonitor, this is per a Singleton pattern. All static class variables are accessed in the
 * normal way. The ConcreteMonitor also synchronizes all methods that write input to the socket in order to
 * properly queue by access time all requests.
 *
 * Finally, the monitor maintains a reference to the MessageListener thread.  This is to spawn the listener upon
 * construction and also to simplify shutdown.  By calling shutdown() the monitor will kill the
 * MessageListener cleanly.
 * 
 * @author Elliot Cash
 */
public class Monitor
{
    private final int BLOCK_RANGE_MAX = 9; //Constants defined for ease in conditionals.
    private final int BLOCK_RANGE_MIN = 1;
    private final int SWITCH_RANGE_MAX = 6;
    private final int SWITCH_RANGE_MIN = 1;
    private final int SPEED_MIN = 0;
    private final int SPEED_MAX = 7;
    private final int LED_MIN = 0;
    private final int LED_MAX = 255;
    public final ReentrantLock lock;
    private static Monitor instance; // Singleton pattern, only ONE ConcreteMonitor may exist.
    public static volatile boolean waiting; // Shows if the ConcreteMonitor is "waiting" for a reply.
    private MessageListener mlistener;
    public LinkedList<Sensor> sensors; // The Sensors registered to the Monitor.
    public Socket s; // The Socket the ConcreteMonitor communicates with.
    public PrintStream osw; // The PrintStream object that writes input to the socket.
    public BufferedReader br; // The BufferedReader which receives output from the socket.
    public static LinkedList<Message> sensorMessages,  // Lists of respective Message types.
        blockMessages, switchMessages, handControlMessages, 
        ledMessages, otherMessages;
    
    /**
     * Gets the only instance of the ConcreteMonitor.
     * 
     * @return the instance variable which calls the private constructor within the Monitor.
     */
    public static Monitor getInstance()
    {
        if(instance == null)
        {
            instance = new Monitor();
        }
        return instance;
    }
    
    private Monitor()
    {
        try
        {
            //s = new Socket("localhost", 6543); //Local sim
            s = new Socket("168.26.219.223", 6543); //Remote sim on my laptop.
            osw = new PrintStream(s.getOutputStream(),true);
            br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            sensors = new LinkedList<Sensor>();
            sensorMessages = new LinkedList<Message>();
            blockMessages = new LinkedList<Message>();
            switchMessages = new LinkedList<Message>();
            handControlMessages = new LinkedList<Message>();
            ledMessages = new LinkedList<Message>();
            otherMessages = new LinkedList<Message>();
            startListener();
        }
        catch(UnknownHostException uhe)
        {
            System.err.println(uhe.getMessage());
        }
        catch(IOException ioe)
        {
            System.err.println(ioe.getMessage());
        }
        lock = new ReentrantLock(true);
    }


    /**
     * Stops the monitor and also notifies the MessageListener
     * to close down the socket and stop.
     */
    public synchronized void shutdown()
    {
        mlistener.end();
        try
        {
            s.close();
        }
        catch (IOException ioe)
        {
            System.err.println(ioe.getMessage());
        }
    }
    
    /**
     * Sets direction and speed attributes to a block on the track.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @param direction one of the instantiated object of the Direction enum in the track package.
     * @param speed a speed set to the specific block with a range of 0-7.
     */
    public void setBlock(int id, Direction direction, int speed)
    {
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX)
        {
           if(speed >= SPEED_MIN && speed <= SPEED_MAX)
           {
               String messageStr = "";
                switch(direction)
                {
                    case CLOCKWISE:
                        messageStr = "*B" + id + "{CW," + speed + "}";
                        lock.lock();
                        osw.println(messageStr);
                        waitForResponse();
                        lock.unlock();
                        break;
                    case COUNTERCLOCKWISE:
                        messageStr = "*B" + id + "{CCW," + speed + "}";
                        lock.lock();
                        osw.println(messageStr);
                        waitForResponse();
                        lock.unlock();
                        break;
                    default:
                        System.err.println("Bad direction arg.");
                }
                Engine.getInstance().postMessage(
                                new Message(messageStr), true);
           }
           else
           {
               System.err.println("Bad speed specified.");
           }
        }
        else
        {
            System.err.println("Bad block specified.");
        }
                    
    }

    /**
     * Retrieves a block's Direction.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @return the Block corresponding to the id input.
     */
    public Direction getBlockDirection(int id)
    {
        Direction returnValue = Direction.COUNTERCLOCKWISE;
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX)
        {
            String messageStr = "*B" + id + "{?}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
            if(blockMessages.removeLast().getArg(MessageArg.BLOCK_DIRECTION).equals("CW"))
            {
                returnValue = Direction.CLOCKWISE;
            }
        }
        else
        {
            System.err.println("Bad block specified.");
        }
        return returnValue;
    }

    /**
     * Retreives the current speed set a block.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @return an int representing the current set speed of the block.
     */
    public int getBlockSpeed(int id)
    {
        int returnValue = -1;
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX )
        {
            String messageStr = "*B" + id + "{?}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
            returnValue = Integer.parseInt(blockMessages.removeLast().getArg(MessageArg.BLOCK_SPEED));
        }
        else
        {
            System.err.println("Bad block specified.");
        }
        return returnValue;
    }
    
    /**
     * Sets a switch on the track to specified SwitchAngle.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     * @param switchAngle one of the instantiated object of the SwitchAngle enum in the track package.
     */
    public void setSwitch(int id, SwitchAngle switchAngle)
    {
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            String messageStr = "";
            switch(switchAngle)
            {
                case NINETY_DEGREES:
                    messageStr = "*T" + id + "{90}";
                    lock.lock();
                    osw.println(messageStr);
                    waitForResponse();
                    lock.unlock();
                    break;
                case ONE_EIGHTY_DEGREES:
                    messageStr = "*T" + id + "{180}";
                    lock.lock();
                    osw.println(messageStr);
                    waitForResponse();
                    lock.unlock();
                    break;
                default:
                    System.err.println("Bad angle specified.");
            }
            Engine.getInstance().postMessage(new Message(messageStr), true);
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
    }

    /**
     * Retrieves a switch's current angle.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     * @return a SwitchAngle corresponding to the id input.
     */
    public SwitchAngle getSwitchAngle(int id)
    {
        SwitchAngle returnValue = SwitchAngle.ONE_EIGHTY_DEGREES;
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            String messageStr = "*T" + id + "{?}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
            if(switchMessages.removeLast().getArg(MessageArg.SWITCH_ANGLE).equals("90"))
            {
                returnValue = SwitchAngle.NINETY_DEGREES;
            }
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
        return returnValue;
    }

    /**
     * Toggles a Switch.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     */
    public void toggleSwitch(int id)
    {
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            String messageStr = "*T" + id + "{T}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
    }

    /**
     * Sets the physical LED lights (There exist 8 individual lights on the LED) 
     * 
     * @param value an integer with a range of 0-255.
     * <table border=1><caption><i>Examples</i></caption><tr><td>Value</td><td>Binary Rep</td><td>On Lights</td></tr>
     * <tr><td>0</td><td>00000000</td><td>None</td></tr>
     * <tr><td>1</td><td>00000001</td><td>First</td></tr>
     * <tr><td>2</td><td>00000010</td><td>Second</td></tr>
     * <tr><td>5</td><td>00000101</td><td>First, Third</td></tr>
     * <tr><td>8</td><td>00001000</td><td>Fourth</td></tr>
     * <tr><td>48</td><td>00110000</td><td>Fifth, Sixth</td></tr>
     */
    public void setLED(int value)
    {
        if(value >= LED_MIN && value <= LED_MAX)
        {
            String messageStr = "*L{" + value + "}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
        }
        else
        {
            System.err.println("Bad value specified.");
        }
    }

     /**
      * Gets the value of the LED
      * 
      * @return an integer representing the value of the LED.
      */
    public int getLED()
    {
        int returnValue;
        String messageStr = "*L{?}";
        lock.lock();
        osw.println(messageStr);
        waitForResponse();
        lock.unlock();
        Engine.getInstance().postMessage(new Message(messageStr), true);
        returnValue = Integer.parseInt(ledMessages.removeLast().getArg(MessageArg.LED_VALUE));
        return returnValue;
    }

    /**
     * This tells the monitor to send out for a HandControl message
     * 
     * @param id an integer representing the id of the HandControl
     */
    public void getHandControlStatus(int id)
    {
        if(id == 1 || id == 2)
        {
            String messageStr = "*U" + id + "{?}";
            lock.lock();
            osw.println(messageStr);
            waitForResponse();
            lock.unlock();
            Engine.getInstance().postMessage(new Message(messageStr), true);
        }
        else
        {
            System.err.println("Bad HandController id specified");
        }
    }
    
    /**
     * Sends the stop message to the sever, this cuts power to
     * all of the blocks.
     */
    public synchronized void stop()
    {
        String messageStr = "*M{STOP}";
        lock.lock();
        osw.println(messageStr);
        waitForResponse();
        lock.unlock();
        Engine.getInstance().postMessage(new Message(messageStr), true);
    }

    /**
     * Registers a sensor to the Monitor.
     *
     * @param sensor A Sensor object to be registered to the Monitor.
     */
    public synchronized void registerSensor(Sensor sensor)
    {
        sensors.add(sensor);
    }

    /**
     * Tests to see if there are any sensor Messages in the Monitor.
     *
     * @return a boolean value. Returns true if the Monitor contains sensor
     * Messages, and returns false if it is empty.
     */
    public synchronized boolean hasSensorMessage()
    {
        return !sensorMessages.isEmpty();
    }

    /**
     * Retrieves the most current sensor Message in the Monitor.
     *
     * @return the last sensor Message retreived by the Listener.
     */
    public synchronized Message getSensorMessage()
    {
        return sensorMessages.removeLast();
    }


    /**
     * Starts the MessageListener thread.
     */    
    private synchronized void startListener()
    {
        this.mlistener = new MessageListener(br);
        mlistener.setName("listener");
        mlistener.start();
    }
    
    /**
     * Halt's the Monitor's thread in order to wait for a response from the
     * MessageListener.
     */
    private synchronized void waitForResponse()
    {
        try
        {
            waiting = true;
            while(waiting)
            {
                wait();
            }
        }
        catch(InterruptedException ex)
        {
            System.err.println(ex.getMessage());
            ex.printStackTrace();
        }
    }
    
    /**
     * Notifies the Monitor that a reply has been received and unhalts the Thread.
     */
    public synchronized void notifyForResponse()
    {
        waiting = false;
        notify();
    }

    /**
     * Retrieves the MessageListener object.
     * @return A MessageListener object
     */
    public synchronized MessageListener getMessageListener()
    {
        return mlistener;
    }
}
